#!/usr/bin/python3 -tt

import argparse
import collections
import ctypes
import errno
import logging
logger = logging.getLogger('mount')
import os
import pdb
from stat import S_IFDIR

import cpfs
from fuse import fuse_get_context
import fuse

def unpack_mode(mode):
    return mode >> 12, mode & 0o7777

def set_stat(st, inode):
    ctypes.memset(ctypes.pointer(st), 0, ctypes.sizeof(st))
    for attr in 'ino', 'nlink', 'uid', 'gid', 'rdev':
        if attr in inode:
            setattr(st, 'st_' + attr, inode[attr])
    if 'perms' in inode:
        st.st_mode = inode['type'] << 12 | inode['perms']
    if 'data' in inode:
        st.st_size = inode['data']['size']
    for f in 'a', 'm', 'c':
        attr = f + 'time'
        if attr in inode:
            ts = getattr(st, 'st_' + f + 'timespec')
            t = inode[f + 'time']
            ts.tv_sec = t // 1000000
            ts.tv_nsec = t % 1000000

def log_repr(level, obj):
    return repr(obj)

def hex_repr(level, obj):
    return hex(0 if obj is None else obj)

def buffer_log_repr(level, obj):
    if level < logging.INFO:
        return repr(obj)
    else:
        assert isinstance(obj, bytes), type(obj)
        return '%d bytes' % len(obj)

def op_wrapper(method):
    log_level = logging.INFO
    arg_reprs = tuple(method.__annotations__.get(a, log_repr) for a in method.__code__.co_varnames[1:method.__code__.co_argcount])
    method_name = method.__name__
    def wrapper(self, *args):
        logger = self.logger
        effective_logger_level = logger.getEffectiveLevel()
        if logger.isEnabledFor(log_level):
            logger.log(log_level, '%s(%s)', method_name, ', '.join(a(effective_logger_level, b) for a,b in zip(arg_reprs, args)))
        try:
            retval = method(self, *args)
        except cpfs.Error as fserr:
            retlog = fserr
            retval = -{
                cpfs.error.InvalidPath: errno.EINVAL,
                cpfs.error.NoEntity: errno.ENOENT,
                cpfs.error.BadFileHandle: errno.EBADF,
                cpfs.error.NoSpace: errno.ENOSPC,
                cpfs.error.NameTooLong: errno.ENAMETOOLONG,
                cpfs.error.NotEmpty: errno.ENOTEMPTY,
                cpfs.error.NameExists: errno.EEXIST,
            }[fserr.__class__]
        else:
            retlog = method.__annotations__.get('return', log_repr)(effective_logger_level, retval)
        logger.log(log_level, '%s->%s', method_name, retlog)
        return retval
    return wrapper


class CpfsFuseOps:

    logger = logger

    def __init__(self, fs):
        self._fs = fs

    @op_wrapper
    def chmod(self, path, mode):
        type, perms = unpack_mode(mode)
        self._fs.single_op.chmod(path.decode('utf-8'), perms)

    @op_wrapper
    def chown(self, path, uid, gid):
        self._fs.single_op.chown(path.decode('utf-8'), uid, gid)

    @op_wrapper
    def destroy(self, priv: hex_repr):
        self._fs.close()

    @op_wrapper
    def fgetattr(self, path, stat, fi):
        assert fi.fh > 0, fi.fh
        inode = self._fs.single_op.fstat(fi.fh - 1)
        set_stat(stat, inode)

    # cpfs flushes after every write, nothing to be done
    @op_wrapper
    def flush(self, path, fi):
        pass

    # only useful for network filesystems, implemented locally if not given
    lock = None

    @op_wrapper
    def getattr(self, path, stat):
        fs_stat = self._fs.single_op.stat(path.decode('utf-8'))
        set_stat(stat, fs_stat)

    @op_wrapper
    def init(self, conn: hex_repr):
        pass

    @op_wrapper
    def link(self, target, path):
        self._fs.single_op.link(target.decode('utf-8'), path.decode('utf-8'))

    @op_wrapper
    def mkdir(self, path, mode):
        type, perms = unpack_mode(mode)
        assert not type & ~S_IFDIR, oct(type)
        self._fs.single_op.mkdir(path.decode('utf-8'), perms, *fuse_get_context()[0:2])

    @op_wrapper
    def rmdir(self, path):
        self._fs.single_op.remove(path.decode('utf-8'))

    @op_wrapper
    def opendir(self, path, fi):
        #assert fi.fh == 0, fi.fh
        fi.fh = 1 + self._fs.single_op.opendir(path.decode('utf-8'))
        assert fi.fh > 0, fi.fh

    @op_wrapper
    def readdir(self, path, filler, offset, fi, stat_cls):
        del path
        while True:
            name, inode, offset = self._fs.single_op.readdir(fi.fh - 1, offset)
            if offset == 0:
                break
            stat = stat_cls()
            set_stat(stat, inode)
            if filler(name.encode('utf-8'), stat, offset) != 0:
                break

    @op_wrapper
    def releasedir(self, path, fi):
        del path
        self._fs.single_op.releasedir(fi.fh - 1)

    @op_wrapper
    def statfs(self, path, statfs):
        ctypes.memset(ctypes.pointer(statfs), 0, ctypes.sizeof(statfs))

        #~ ('f_bsize', c_ulong),
        #~ ('f_frsize', c_ulong),
        #~ ('f_blocks', c_fsblkcnt_t),
        #~ ('f_bfree', c_fsblkcnt_t),
        #~ ('f_bavail', c_fsblkcnt_t),
        #~ ('f_files', c_fsfilcnt_t),
        #~ ('f_ffree', c_fsfilcnt_t),
        #~ ('f_favail', c_fsfilcnt_t)]
        statfs.f_bsize = 512
        statfs.f_frsize = 512
        statfs.f_blocks = self._fs._geo.data_blocks
        statfs.f_files = self._fs._geo.data_blocks
        statfs.f_namemax = 255

    @op_wrapper
    def mknod(self, path, mode, rdev):
        self._fs.single_op.mknod(*
            (path.decode('utf-8'),) +
            unpack_mode(mode) +
            (rdev,) +
            fuse_get_context()[0:2])

    @op_wrapper
    def open(self, path, fi):
        fi.fh = 1 + self._fs.single_op.open(path.decode('utf-8'), *fuse_get_context()[0:2])

    @op_wrapper
    def create(self, path, mode, fi):
        self.logger.debug('create(fi.flags=%o)', fi.flags)
        assert fi.fh == 0
        fi.fh = 1 + self._fs.single_op.open(
            *((path.decode('utf-8'),) + fuse_get_context()[0:2] + unpack_mode(mode)),
            create=True)

    @op_wrapper
    def read(self, path, buf, count, offset, fi):
        buf2 = self._fs.single_op.read(fi.fh - 1, count, offset)
        assert len(buf2) <= count, (len(buf2), count)
        ctypes.memmove(buf, bytes(buf2), len(buf2))
        return len(buf2)

    @op_wrapper
    def readlink(self, path, buf, size):
        buf2 = self._fs.single_op.readlink(path.decode('utf-8'), size) + b'\0'
        ctypes.memmove(buf, bytes(buf2), min(size, len(buf2)))

    @op_wrapper
    def release(self, path, fi):
        del path
        self._fs.single_op.close(fi.fh - 1)

    @op_wrapper
    def rename(self, oldpath, newpath):
        self._fs.single_op.rename(oldpath.decode(), newpath.decode())

    @op_wrapper
    def symlink(self, target, path):
        uid, gid = fuse_get_context()[0:2]
        self._fs.single_op.symlink(target, path.decode('utf-8'), uid, gid)

    @op_wrapper
    def unlink(self, path):
        self._fs.single_op.remove(path.decode('utf-8'))

    @op_wrapper
    def utimens(self, path, times):
        self._fs.single_op.utimes(path.decode('utf-8'), access=times[0], modify=times[1])

    @op_wrapper
    def write(self, path, buf: buffer_log_repr, offset, fi):
        return self._fs.single_op.write(fi.fh - 1, buf, offset)

    @op_wrapper
    def ftruncate(self, path, length, fi):
        self._fs.single_op.ftruncate(fi.fh - 1, length)

    @op_wrapper
    def truncate(self, path, length):
        self._fs.single_op.truncate(path.decode(), length)


def main():
    formatter = logging.Formatter(logging.BASIC_FORMAT)
    handler = cpfs.util.FrigidStreamHandler()
    handler.setFormatter(formatter)
    logging.root.addHandler(handler)
    logger.setLevel(logging.INFO)

    parser = argparse.ArgumentParser()
    parser.add_argument('device', nargs='?')
    parser.add_argument('mount_point', nargs='?')
    parser.add_argument('--print-fuse-opts', default=False, action='store_true')
    args = parser.parse_args()
    if args.print_fuse_opts:
        fuse.print_fuse_opts()
        return

    argv = ['fuse_argv0', '-f', '-s',
        '-ouse_ino,allow_other,default_permissions,big_writes',
        '-ofsname='+args.device,
        '-osubtype=cpfs',
        args.mount_point]

    fuse.FUSE(CpfsFuseOps(cpfs.Filesystem(args.device)), argv)
    logger.debug('Returned from fuse.FUSE')


if __name__ == '__main__':
    main()
